This document is a efford to introduce the strengths and benefits of functional programming in scala.
We do not claim intellectual property of all the material presented. We specifically refer to the original resources whenever is needed.
The presentation path of the concepts is still under consideration and may be changed in future reviews.
Example heavily influenced by Functional and Reactive Domain Modeling
In [1]:
//Model as an ADT
type Comments = Seq[String]
val emptyComments = Seq.empty[String]
sealed trait TicketStatus
case object Open extends TicketStatus
case object InProgress extends TicketStatus
case object Closed extends TicketStatus
//Aggregate root
case class Ticket(no:String, status: TicketStatus, title: String, comments: Comments)
A simple domain service
In [28]:
// Service attempt #1
//Let's define a service that describes our algebra
trait TicketService {
//Open a ticket
def open(no: String, title: String): Ticket
//Make a ticket in progress
def start(no: String): Ticket
//Change the title
def changeTitle(no: String, title: String): Ticket
//Close ticket
def close(no: String): Ticket
}
//What we can do with this algebra?
//Open a ticket with no = "t1", title ="..."
// start this ticket
// update this ticket title = "...."
// close this ticket
def program1(ts: TicketService): Ticket = {
val t = ts.open("t1", "...") //Every step returns an immutable ticket with the changes
val t1 = ts.start(t.no)
val t2 = ts.changeTitle(t1.no, "Ticket updated")
val t3 = ts.close(t2.no)
t3
}
program1 _
In [ ]:
//Composition basics
//Methods
def f(n: String) = n + n
def g(n: String) = n.length
//Functions
// (String => String)
val ff = (n: String) => n + n
// (String => Int)
val gf = (n: String) => n.length
//Composition
val gof = (g _).compose(f _)
val gof1 = (f _).andThen(g _)
//Composition with lambda
val folambda = (g _).compose((x:String) => x + x + x)
f("1")
gof("1")
folambda("foo")
In [ ]:
//Service attempt #1.1
//Given that lets re-write our program
def program1(ts: TicketService): Ticket = {
val chain = (ts.open((_:String), "..."))
.andThen(t => ts.start(t.no))
.andThen(t => ts.changeTitle(t.no, "...."))
.andThen(t => ts.close(t.no))
chain("t1")
}
program1 _
In [ ]:
trait TicketRepository {
def query(no: String): Ticket
def store(t: Ticket): Ticket
}
Using the TicketRepository in the TicketService.
In [ ]:
//Service attempt #2
trait TicketService {
//Open a ticket
def open(no: String, title: String): TicketRepository => Ticket
//Make a ticket in progress
def start(no: String): TicketRepository => Ticket
//Change the title
def changeTitle(no: String, title: String): TicketRepository => Ticket
//Close ticket
def close(no: String): TicketRepository => Ticket
}
//Now our programs have a ticket service and repo
// Let's try to compose them
def program2(s: TicketService, r: TicketRepository) = {
val chain = (s.open(_:String, "...")(r))
.andThen(t => s.start(t.no)(r))
.andThen(t => s.changeTitle(t.no,"New Title")(r))
.andThen(t => s.close(t.no)(r))
chain("t1")
}
program2 _
We use the the repository variable in each composition because each method returns a (R => T) (e.g. TicketRepository => Ticket)
Generalizing program3 a reusable function composition:
We need a composition function with signature:
<functionName???>(a,f) :: (R => A) => (A => (R => B)) => (R => B)
If we name the type (of function) (R => A) to RD[R,A]
we need something of type:
<functionName???>(a,f) :: RD[R,A] => (A => RD[R,B]) => RD[R,B]
if we fix the first parameter of R type we actually need something of type:
RD[A] => (A => RD[B]) => RD[B]
which resembles the function flatMap of List[A] but we have now a RD[A]
So we can implemenent a parametric construct
RD[R,T]which wraps functions of typeR => Aand supports aflatMapoperation. The implementation of thisflatMapcomposes correctly our enchanced function types.
In [ ]:
//A custom Reader implementation (Reader = RD) in Scala
//Wrapper of functions R => A
case class Reader[R, A](run: R => A) { /* R => A = Function1[R,A] in scala*/
//Additional map operator
def map[B] (f: A => B): Reader[R,B] = {
Reader(r => f(run(r)))
}
def flatMap[B] (f: A => Reader[R,B]): Reader[R,B] = {
Reader(r => f(run(r)).run(r)) // This is just complex function composition boilerplate...
}
}
Trusting that this implementation is correct we can rewrite our example.
In [ ]:
//Service attempt #3
trait TicketService {
//Open a ticket
def open(no: String, title: String): Reader[TicketRepository, Ticket]
//Make a ticket in progress
def start(no: String): Reader[TicketRepository, Ticket]
//Change the title
def changeTitle(no: String, title: String): Reader[TicketRepository, Ticket]
//Close ticket
def close(no: String): Reader[TicketRepository,Ticket]
}
//Now our programs have a ticket service and repo
//Let's try to compose them
def program3(s: TicketService, r: TicketRepository) = {
// Program 2: for reference and comparison
// val chain = (s.open(_:String, "...")(r))
// .andThen(t => s.start(t.no)(r))
// .andThen(t => s.changeTitle(t.no,"....")(r))
// .andThen(t => s.close(t.no)(r))
// chain("t1")
val chain = { (no:String) =>
s.open(no, "...")
.flatMap(t => s.start(t.no))
.flatMap(t => s.changeTitle(t.no, "...."))
.flatMap(t => s.close(t.no))}
chain("t1").run(r) //Only one usage of r
}
program3 _
Scala has a special syntactic notation for structures that support map and flatMap
In [ ]:
// Service attempt 3.1
def program31(s: TicketService, r: TicketRepository) = {
def chain(no: String): Reader[TicketRepository, Ticket] =
for {
t <- s.open(no, "...")
t <- s.start(t.no)
t <- s.changeTitle(t.no, "....")
t <- s.close(t.no)
} yield t
chain("t1").run(r)
}
program31 _
// Or with a funky name :P
def openStartChangeTitleAndCloseOperation(no:String, s:TicketService): Reader[TicketRepository, Ticket] =
for {
t <- s.open(no, "...")
t <- s.start(t.no)
t <- s.changeTitle(t.no, "....")
t <- s.close(t.no)
} yield t
openStartChangeTitleAndCloseOperation _
//invoke on actual code like
// openStartChangeTitleAndCloseOperation("t1",service).run(repo) where service,repo concrete implementations...
Let's not reinvent the wheel and use a scala functional library
In [ ]:
classpath.add("org.typelevel" %% "cats-core" % "1.0.0-MF")
In [ ]:
// Service attempt #3.2
// Using Cats
import cats.data.Reader
trait TicketService {
def open(no: String, title: String): Reader[TicketRepository, Ticket]
def start(no: String): Reader[TicketRepository, Ticket]
def changeTitle(no: String, title: String): Reader[TicketRepository, Ticket]
def close(no: String): Reader[TicketRepository,Ticket]
}
def program32(no:String, s:TicketService): Reader[TicketRepository, Ticket] =
for {
t <- s.open(no, "...")
t <- s.start(t.no)
t <- s.changeTitle(t.no, "....")
t <- s.close(t.no)
} yield t
program32 _
In [ ]:
import scala.util.Try
// A more realistic repository
trait TicketRepository {
def query(no: String): Try[Option[Ticket]]
def store(t: Ticket): Try[Ticket]
}
In [ ]:
// Service attempt #4
import scala.util.Try
import cats.data.Kleisli
import cats.implicits._
trait TicketService {
def open(no: String, title: String): Kleisli[Try, TicketRepository, Ticket]
def start(no: String): Kleisli[Try, TicketRepository, Ticket]
def changeTitle(no: String, title: String): Kleisli[Try, TicketRepository, Ticket]
def close(no: String): Kleisli[Try, TicketRepository,Ticket]
}
def program4(no:String, s:TicketService): Kleisli[Try, TicketRepository, Ticket] =
for {
t <- s.open(no, "...")
t <- s.start(t.no)
t <- s.changeTitle(t.no, "....")
t <- s.close(t.no)
} yield t
program4 _
Kleisli is an implementation detail we need something more user friendly name with more domain related meaning.
In [ ]:
// Service attempt #4.1
import scala.util.Try
import cats.data.Kleisli
import cats.implicits._
type ServiceResult[A] = Kleisli[Try,TicketRepository,A]
trait TicketService {
def open(no: String, title: String): ServiceResult[Ticket]
def start(no: String): ServiceResult[Ticket]
def changeTitle(no: String, title: String): ServiceResult[Ticket]
def close(no: String): ServiceResult[Ticket]
}
def program4(no:String, s:TicketService): ServiceResult[Ticket] =
for {
t <- s.open(no, "...")
t <- s.start(t.no)
t <- s.changeTitle(t.no, "....")
t <- s.close(t.no)
} yield t
program4 _
In [ ]:
import collection.mutable.{ Map => MMap }
trait InMemoryTicketRepository extends TicketRepository {
lazy val repo = MMap.empty[String, Ticket]
def query(no: String): Try[Option[Ticket]] = Success(repo.get(no))
def store(a: Ticket): Try[Ticket] = {
val r = repo += ((a.no, a))
Success(a)
}
}
//This is the concrete implementation
object InMemoryTicketRepository extends InMemoryTicketRepository
We have also to implement a concrete type for our ticket service.
In [ ]:
//package services.interpreters
object TicketService extends TicketService {
def open(no: String, desc: String) = (r: TicketRepository) =>
r.query(no) match {
case Success(Some(t)) => Failure(new Exception(s"Ticket with $no already exists"))
case Success(None) =>
//validations
if (no.isEmpty) Failure(new Exception(s"Ticket $no should not be empty"))
else if (desc.isEmpty)
else r.store(Ticket(no, Open, desc, process))
case Failure(ex) => Failure(new Exception(s"Failed to open ticket $no: $desc", ex))
}
def changeStatus(no: String, status: Ticket) = (r: TicketRepository) =>
r.query(no) match {
}
}
// def changeStatus(no: String, status: TicketStatus): Ticket = ???
// def changeDescription(no: String, descr: String): Ticket = ???
// def close(no: String): Ticket = ???
// }
val memoryRepo = InMemoryTicketRepository
val TS = TicketService
TS.open("t1", "First ticket",emptyProcess)(memoryRepo)
TS.open("t2", "Second ticket",emptyProcess)(memoryRepo)
memoryRepo.repo
Fotios Paschos, @fpaschos, Sep 2017